<?php

/**
 * This file is part of Switcher.
 * 
 * Switcher is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Switcher is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Switcher. If not, see <http://www.gnu.org/licenses/>.
 * 
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 */

namespace switcher\config;

/**
 * The <b>Config</b> class describes the navigation rules of a Switcher application. It
 * defines the filter stack, user controllers and available renderers. A configuration
 * instance also serves as a <i>factory</i> for filters, controllers, renderers and views.
 * 
 * <p>One should not build a <b>Config</b> manually. Building a complex configuration
 * would represent a large amount of code, hard to maintain, and not really pertinent.
 * Configuration should not be done through PHP code but rather from a textual declaration.
 * This class only serves as a bridge between a text file and other API. The Switcher
 * Framework provides a facility for easy configuration through the <b>XmlConfigFactory</b>.
 * You can easily create a configuration in an XML file and validate it against an XSD schema
 * before using it.</p>
 * 
 * @author Dri <switcher.framework@gmail.com>
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 * @package switcher\config
 * @since 1.0.0
 * @see switcher\config\factories\XmlConfigFactory
 */
final class Config
{
	/**
	 * The relative root, without the leading slash.
	 * 
	 * @var string
	 * @default ''
	 */
	public $root = '';
	
	/**
	 * The name of the default renderer to use when a view doesn't declare any.
	 * 
	 * @var string
	 * @see #renderers
	 */
	public $defaultRenderer = '';
	
	/**
	 * The name of the default controller to use when a view doesn't declare any.
	 * 
	 * @var string
	 * @see #controllers
	 */
	public $defaultController = '';
	
	/**
	 * The name of the view when the path is omitted from the request.
	 * 
	 * @var string
	 * @see #views
	 */
	public $defaultView = '';
	
	/**
	 * Contains all the filters declarations. A <b>Filter</b> declaration is an array
	 * containing a '@type' item, the type of the filter. Other items are injected as
	 * class properties. Items whose names doesn't refer to an existing property are
	 * ignored.
	 * 
	 * <p>Example :
	 * <code>
	 * $myConfig->filters = array(
	 *     array(
	 *         '@type' => 'switcher\filters\CompressionFilter',
	 *         'level' => 6,
	 *         'foo' => 'bar' // this one is ignored
	 *     ),
	 *     array(
	 *         '@type' => 'MyCustomFilter',
	 *         'myCustomProperty' => 'myCustomValue'
	 *     )
	 * );
	 * </code>
	 * </p>
	 * 
	 * @var array
	 */
	public $filters = array();
	
	/**
	 * Contains all the renderers declarations. A <b>Rendered</b> declaration is a named
	 * array containing a '@type' item, the type of the renderer. Other items are injected
	 * as class properties. Items whose names doesn't refer to an existing property are
	 * ignored.
	 * 
	 * <p>Example :
	 * <code>
	 * $myConfig->renderers = array(
	 *     'layout' => array(
	 *         '@type' => 'switcher\renderers\XmlRenderer',
	 *         'template' => 'includes/views/layout.html',
	 *         'contentType' => 'text/html',
	 *         'ajax' => array(
	 *             'namespaces' => array(
	 *                 'x' => 'http://www.w3.org/1999/xhtml'
	 *             ),
	 *             'xpath' => "//x:div[id='content']"
	 *         )
	 *     ),
	 *     'rss' => array(
	 *         '@type' => 'switcher\renderers\XmlRenderer',
	 *         'template' => 'includes/views/rss.xml',
	 *         'contentType' => 'application/rss+xml',
	 *         'foo' => 'bar' // this one is ignored
	 *     ),
	 *     'json' => array(
	 *         '@type' => 'switcher\renderers\JsonRenderer',
	 *     ),
	 *     'custom' => array(
	 *         '@type' => 'MyCustomRenderer',
	 *         'myCustomProperty' => 'myCustomValue'
	 *     )
	 * );
	 * </code>
	 * </p>
	 * 
	 * @var array
	 */
	public $renderers = array();
	
	/**
	 * Contains all the controllers declarations. A <b>Controller</b> declaration is a named
	 * array containing a '@type' item, the type of the controller. Other items are injected
	 * as class properties. Items whose names doesn't refer to an existing property are
	 * ignored. A controller must declare its default endpoint in a '@default-action' item,
	 * It must match a public no-arg method.
	 * 
	 * <p>Example :
	 * <code>
	 * $myConfig->controllers = array(
	 *     'home' => array(
	 *         '@type' => 'switcher\controllers\StubController',
	 *         '@default-action' => 'loadStub',
	 *         'stub' => 'includes/views/home.json',
	 *         'foo' => 'bar' // this one is ignored
	 *     ),
	 *     'custom' => array(
	 *         '@type' => 'MyCustomController',
	 *         '@default-action' => 'myCustomAction',
	 *         'myCustomProperty' => 'myCustomValue'
	 *     )
	 * );
	 * </code>
	 * </p>
	 * 
	 * @var array
	 */
	public $controllers = array();
	
	/**
	 * Contains all the views declarations. A <b>View</b> declaration is a named array. A
	 * view can declare the allowed HTTP methods, the use of AJAX for the request, the optional
	 * and mandatory HTTP (GET or POST) parameters, a <b>Renderer</b> and a <b>Controller</b>.
	 * 
	 * <p>The methods are declared as an array of strings, each string being a standard or
	 * custom HTTP method (such as GET, HEAD, POST etc). The AJAX flag indicates whether the
	 * HTTP request should mention an XmlHttpRequest header. GET and POST parameters must be
	 * declared as an array of named boolean values. The name of an item being the parameter
	 * name and the boolean value being set to true if optional. If there isn't any method
	 * declared for the view, it is then not accessible through a standard request. It is
	 * particularly useful for hiding special pages such as a '404 not found' view.</p>
	 * 
	 * <p>A <b>Renderer</b> and a <b>Controller</b> might be declared too. If not, defaults
	 * are used. The renderer and the controller are declared as named arrays. They might declare
	 * a 'name' item if not referring to the default. They might also declare a 'properties'
	 * item, a named array used to override the object's properties. This you can for example
	 * declare a 'layout' XmlRenderer for your HTML pages. This way, each view can define its
	 * own template and serve it to the 'layout' renderer. Views can also refer to a specific
	 * action of the target <i>Controller</i>.</p>
	 * 
	 * <p>Example :
	 * <code>
	 * $myConfig->views = array(
	 *     'home' => array(
	 *         'methods' => array('get')
	 *         // this one uses the default renderer and controller
	 *     ),
	 *     'article' => array(
	 *         'path' => array('value' => '%article-\d+.html%', 'regexp' => 'true'),
	 *         'methods' => array('get', 'post'),
	 *         'ajax' => 'optional'
	 *         'get-params' => array('article' => false),
	 *         'post-params' => array(
	 *             'title' => false,
	 *             'content' => false
	 *         ),
	 *         'renderer' => array(
	 *             'name' => 'layout',
	 *             'properties' => array('template' => 'includes/views/article.html')
	 *         ),
	 *         'controller' => array(
	 *             'name' => 'article'
	 *         )
	 *     ),
	 *     'comment' => array(
	 *         'methods' => array('post'),
	 *         'get-params' => array('article' => false),
	 *         'post-params' => array(
	 *             'email' => false,
	 *             'www' => true, // commenters might not have a personal website
	 *             'comment' => false
	 *         ),
	 *         'renderer' => array(
	 *             'name' => 'layout',
	 *             'properties' => array('template' => 'includes/views/comment.html')
	 *         ),
	 *         'controller' => array(
	 *             'name' => 'article'
	 *         ),
	 *         'action' => 'comment'
	 *     ),
	 *     'feed' => array(
	 *         'path' => array('value' => 'feed'),
	 *         'methods' => array('get'),
	 *         'renderer' => array('name' => 'rss'),
	 *         'controller' => array('name' => 'rss')
	 *     ),
	 *     '404' => array(
	 *         'methods' => array(), // visible only in case of a NotFoundException
	 *         'renderer' => array(
	 *             'name' => 'layout',
	 *             'properties' => array('template' => 'includes/views/404.html')
	 *         ),
	 *         'controller' => array(
	 *             'name' => 'mock',
	 *             'properties' => array('mock', 'includes/view/404.json')
	 *         )
	 *     )
	 * );
	 * </code>
	 * </p>
	 * 
	 * @var array
	 * @see #$renderers
	 * @see #$controllers
	 */
	public $views = array();
	
	/**
	 * Logger.
	 * @var switcher\log\Logger
	 */
	private $logger;
	
	/**
	 * No-arg constructor.
	 */
	public function __construct()
	{
		\switcher\log\LoggerFactory::injectLogger($this);
	}
	
	/**
	 * Compute the absolute root from the declared root.
	 * 
	 * @return string The absolute root, with the leading slash.
	 */
	public function root()
	{
		return $this->root == '' ? '/' : '/' . $this->root . '/';
	}
	
	/**
	 * Lookup the first view matching the request URI.
	 * 
	 * @return string The name of the view or null if no match found
	 */
	public function findView()
	{
		$path = \switcher\utils\HttpHelper::getRequestPath();
		
		if ($path == '')
		{
			return $this->defaultView;
		}
		
		return $this->findNonDefaultView($path);
	}
	
	/**
	 * Lookup the first view matching the request URI.
	 * 
	 * @param string $path The request URI
	 * @return string The name of the view or null if no match found
	 */
	private function findNonDefaultView($path)
	{
		$this->logger->info('Searching view : ' . $path);
		
		foreach ($this->views as $name => $view)
		{
			if ( $this->matchViewPath($view, $path) )
			{
				return $name;
			}
		}
		
		return null;
	}
	
	/**
	 * <p>Matches a request URI against a view declaration.
	 * The view might declare either regexp or shell wildcard path.</p>
	 * 
	 * @param string $view The view declaration
	 * @param string $path The request URI
	 * @return bool True if the request URI matches the view
	 * @see switcher\utils\FileHelper::match()
	 */
	private function matchViewPath($view, $path)
	{
		if ( !@isset($view['path']['value']) )
		{
			return false;
		}
		
		$viewPath = $view['path']['value'];
		
		if ($viewPath == null || $viewPath == '')
		{
			return false;
		}
		
		$regexp = @isset($view['path']['regexp']) && $view['path']['regexp'] == 'true';
		
		$this->logger->info('Matching view : [' . $path . '] [' . $viewPath . ']');
		
		if ($regexp)
		{
			return preg_match($viewPath, $path) !== 0;
		}
		
		return \switcher\utils\FileHelper::match($viewPath, $path, FNM_PATHNAME) !== false;
	}
	
	/**
	 * Creates a view, its controller and renderer.
	 * 
	 * @param string $name The name of the view to create
	 * @return View The view corresponding to the $name
	 * @throws \switcher\exceptions\NotFoundException If the view isn't declared
	 */
	public function getView($name)
	{
		if ( $name == null || !@isset($this->views[$name]) )
		{
			throw new \switcher\exceptions\NotFoundException('View Not Found : ' . $name);
		}
		
		// create the view
		$view = new View();
		$viewConfig = $this->views[$name];
		
		// get request config
		$view->path = $this->extractViewPath($viewConfig);
		$view->methods = $viewConfig['methods'];
		$view->ajax = $viewConfig['ajax'];
		$view->getParameters = $viewConfig['get-params'];
		$view->postParameters = $viewConfig['post-params'];
		
		// create the renderer and the controller
		$this->createViewObject($viewConfig, $view, 'renderer', 'switcher\renderers\Renderer', $this->defaultRenderer);
		$this->createViewObject($viewConfig, $view, 'controller', 'switcher\controllers\Controller', $this->defaultController);
		
		$view->action = $this->getAction($viewConfig);
		
		// inject additional properties in the controller
		\switcher\utils\ReflectionHelper::setProperty($view->controller, 'renderer', $view->renderer);
		\switcher\utils\ReflectionHelper::setProperty($view->controller, 'view', $name);
		\switcher\utils\ReflectionHelper::setProperty($view->controller, 'root', $this->root());
		\switcher\utils\ReflectionHelper::setProperty($view->controller, 'path', $view->path);
		
		return $view;
	}
	
	/**
	 * Extract path information for the view.
	 * @param array $view The view configuration
	 */
	private function extractViewPath($view)
	{
		$path = \switcher\utils\HttpHelper::getRequestPath();
		
		$viewPath = $view['path']['value'];
		$regexp = @isset($view['path']['regexp']) && $view['path']['regexp'] == 'true';
		
		if ($regexp)
		{
			preg_match($viewPath, $path, $matches);
		}
		else
		{
			$matches = \switcher\utils\FileHelper::match($viewPath, $path, FNM_PATHNAME); 
		}
		
		return array('pattern' => $viewPath, 'matches' => $matches, 'regexp' => $regexp);
	}
	
	/**
	 * Creates a <b>Renderer</b> or a <b>Controller</b>. The method is generic and might be used
	 * to create other kinds of future view objects.
	 * 
	 * @param $viewConfig The view declared in the <i>$views</i> property
	 * @param $view The <b>View</b> object being created
	 * @param $type The type of view object to create ()
	 * @param $class The base class of the view object
	 * @param $default The object to use if the view doesn't declare any
	 */
	private function createViewObject($viewConfig, $view, $type, $class, $default)
	{
		if ( @isset($viewConfig[$type]['name']) )
		{
			$objectName = $viewConfig[$type]['name'];
		}
		else
		{
			$objectName = $default;
		}
		
		$objectConfig = $this->{$type . 's'}[$objectName];
		$object = \switcher\utils\ReflectionHelper::createObject($objectConfig);
		
		if ( !is_subclass_of($object, $class) )
		{
			throw new \InvalidArgumentException('Not a ' . $type . ' : ' . get_class($object));
		}
		
		if ( @isset($viewConfig[$type]['properties']) )
		{
			$objectProperties = $viewConfig[$type]['properties'];
			\switcher\utils\ReflectionHelper::overrideObject($object, $objectProperties);
		}
		
		\switcher\log\LoggerFactory::injectLogger($object);
		
		$view->{$type} = $object;
	}
	
	/**
	 * Get the view action or the controller's default action, or the default controller's
	 * default action.
	 * 
	 * @param $viewConfig The view declared in the <i>$views</i> property
	 */
	private function getAction($viewConfig)
	{
		if ( !empty( $viewConfig['action'] ) )
		{
			return $viewConfig['action'];
		}
		
		$controller = !empty($viewConfig['controller']) ? $viewConfig['controller']['name'] : $this->defaultController;
		
		return $this->controllers[$controller]['@default-action'];
	}
}

?>